package utils.advancedSocket;
import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;

/**
 * Permet de rajouter des listeners et une <i>petite</i> API de gestion pour une
 * {@link Socket}.
 * 
 * @author Bertholet Damien & Daoust Julien
 */
public class AdvancedSocket implements Closeable {

	private List<AdvancedSocketListener> advancedSocketListeners =
	        new ArrayList<AdvancedSocketListener>();

	private Thread listenThread;
	private Socket socket;
	private PrintWriter printer;
	private BufferedReader reader;

	private boolean closed = false;
	private Object closeLock = new Object();

	/**
	 * Constructeur.
	 * 
	 * @param socket
	 *            Le {@link Socket} de communication.
	 * @throws IOException
	 *             S'il y a une erreur avec la {@link Socket}.
	 */
	public AdvancedSocket(Socket socket) throws IOException {
		this.socket = socket;

		reader =
		        new BufferedReader(new InputStreamReader(
		                socket.getInputStream()));
		printer = new PrintWriter(socket.getOutputStream(), true);

		listen();
	}

	/**
	 * Ecoute les messages recues, caracteres par carateres.
	 */
	private void listen() {
		Runnable runnable = new Runnable() {

			@Override
			public void run() {
				while(true)
					try {
						int id = reader.read();
						// C'est la fin de la connexion car l'autre 'cote' a
						// ferme sa connexion.
						if(id == -1) {
							close();
							break;
						}

						onReceiving((char) id);
					} catch (IOException e) {
						closeConnectionError("Connection stop by user.");
						break;
					}
			}
		};

		listenThread = new Thread(runnable);
		listenThread.start();
	}

	/**
	 * Envoie les donnees.
	 * 
	 * @param data
	 *            Les donnees.
	 */
	public void send(String data) {
		printer.println(data);
		onSending(data);
	}

	/**************************************************************************
	 **************************** Close Connection ****************************
	 *************************************************************************/
	/**
	 * Ferme la connexion a cause d'une erreur. Cela appelera la fonction
	 * 'onCloseConnectionError' des {@link AdvancedSocketListener} de cet
	 * {@link AdvancedSocket}.
	 * 
	 * @param message
	 *            Le message de l'erreur.
	 */
	public void closeConnectionError(String message) {
		synchronized (closeLock) {
			if(isClosed())
				return;
		}

		onCloseConnectionError(message);
		closeConnection();
	}

	@Override
	public void close() {
		synchronized (closeLock) {
			if(isClosed())
				return;
		}

		onCloseConnection();
		closeConnection();
	}

	/**
	 * Ferme la connection.
	 */
	private void closeConnection() {
		synchronized (closeLock) {
			if(isClosed())
				return;

			closed = true;
			closeAll();
		}
	}

	/**
	 * Retourne l'etat de l'{@link AdvancedSocket}.
	 * 
	 * @return True si l'{@link AdvancedSocket} est ferme. ; False sinon.
	 */
	public boolean isClosed() {
		synchronized (closeLock) {
			return closed;
		}
	}

	/**
	 * Ferme toutes les connexions.
	 */
	private void closeAll() {
		try {
			printer.close();
			listenThread.interrupt();
			reader.close();
			socket.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**************************************************************************
	 ****************************** advancedSocketListeners ******************************
	 *************************************************************************/
	/**
	 * Ajouter un {@link AdvancedSocketListener}.
	 * 
	 * @param listener
	 *            Le {@link AdvancedSocketListener}.
	 */
	public void addAdvancedSocketListener(AdvancedSocketListener listener) {
		advancedSocketListeners.add(listener);
	}

	/**
	 * Supprimer un {@link AdvancedSocketListener}.
	 * 
	 * @param listener
	 *            Le {@link AdvancedSocketListener}.
	 */
	public void removeAdvancedSocketListener(AdvancedSocketListener listener) {
		advancedSocketListeners.remove(listener);
	}

	/**
	 * Renvoie la liste des {@link AdvancedSocketListener}.
	 * 
	 * @return La liste des {@link AdvancedSocketListener}.
	 */
	public List<AdvancedSocketListener> getAdvancedSocketListeners() {
		return advancedSocketListeners;
	}

	/**
	 * Appeler la fonction "onReceive" de tous les
	 * {@link AdvancedSocketListener}.
	 * 
	 * @param c
	 *            Le caractere recue.
	 */
	private void onReceiving(char c) {
		AdvancedSocketEvent event = new AdvancedSocketEvent(this, c);
		for(AdvancedSocketListener listener: advancedSocketListeners)
			listener.onReceiving(event);
	}

	/**
	 * Appeler la fonction "onSending" de tous les
	 * {@link AdvancedSocketListener}.
	 * 
	 * @param data
	 *            Les donnees envoyes.
	 */
	private void onSending(String data) {
		AdvancedSocketEvent event = new AdvancedSocketEvent(this, data);
		for(AdvancedSocketListener listener: advancedSocketListeners)
			listener.onSending(event);
	}

	/**
	 * Appeler la fonction "onCloseConnection" de tous les
	 * {@link AdvancedSocketListener}.
	 * 
	 * @param message
	 *            Le message d'erreur.
	 */
	private void onCloseConnectionError(String message) {
		AdvancedSocketCloseEvent event =
		        new AdvancedSocketCloseEvent(this, message);
		for(AdvancedSocketListener listener: advancedSocketListeners)
			listener.onCloseConnection(event);
	}

	/**
	 * Appeler la fonction "onCloseConnection" de tous les
	 * {@link AdvancedSocketListener}.
	 */
	private void onCloseConnection() {
		AdvancedSocketCloseEvent event = new AdvancedSocketCloseEvent(this);
		for(AdvancedSocketListener listener: advancedSocketListeners)
			listener.onCloseConnection(event);
	}
}